本文是《Flask-Web开发:基于Python的Web应用开发实战》的个人学习笔记
目录:
- 安装
- 使用虚拟环境
- 使用pip安装Python包
- 程序的基本结构
- 初始化
- 路由和视图函数
- 启动服务器
- 一个完整的循环
- 请求 - 响应循环
- 程序和请求上下文
- 请求调度
- 请求钩子
- 响应
- Flask扩展
- 模板
- JinJa2 模板引擎
- 渲染模板
- 变量
- 控制结构
- 使用Flask-Bootstrap 集成 Twitter Bootstrap
- 自定义错误页面
- 链接
- 静态文件
- 使用Flask-Monment本地化日期和时间
- JinJa2 模板引擎
- Web表单
- 跨站请求伪造保护
- 表单类
- 把表单渲染成HTML
- 在视图函数中处理表单
- 重定向和用户会话
- Flash消息
- 数据库
- 电子邮件
- 大型程序的结构
- 项目结构
- 配置选项
- 程序包
- 使用程序工厂函数
- 在蓝本中实现程序功能
- 启动脚本
- 需求文件
- 单元测试
- 创建数据库
- 用户认证
- Flask的认证扩展
- 密码安全性
- 创建认证蓝本
- 使用Flask-Login认证用户
- 准备用于登录的用户模型
- 保护路由
- 添加登录表单
- 登入用户
- 登出用户
- 测试登录
- 注册新用户
- 添加用户注册表单
- 注册新用户
- 确认账户
- 使用isdangerous生成确认令牌
- 发送确认邮件
- 管理账户
- 用户角色
- 角色在数据库中的表示
- 赋予角色
- 角色验证
- 用户资料
- 资料信息
- 用户资料页面
- 资料编辑器
- 用户级别的资料编辑器
- 管理员级别的资料编辑器
- 用户头像
- 博客文章
- 关注者
- 用户评论
- 应用编程接口
- 测试
- 性能
- 部署
- 其他
#1安装
##1.1 使用虚拟环境
虚拟环境是Python解释器的一个私有副本,在这个环境中你可以安装私有包,而且不会影响系统中安装的全局Python解释器。
虚拟环境非常有用,可以在系统的Python解释器中避免包的混乱和版本的冲突。为每个程序单独创建虚拟环境可以保证程序只能访问虚拟环境中的包,从而保持全局解释器的干净整洁,使其只作为创建(更多)虚拟环境的源。使用虚拟环境还有个好处,那就是不需要管理员权限。
虚拟环境使用第三方实用工具 virtualenv 创建。输入以下命令可以检查系统是否安装了 virtualenv:
$ virtualenv --version
当你有多个项目的时候,有可能会遇到不同项目所依赖的包的版本不一致的情况,糟糕的是可能会有某些关键的外部库前后两个版本并不兼容,这个时候virtualenv就很有用处,可以为每个项目创建一个虚拟环境,这样各个项目之间就不会冲突
如果结果显示错误,你就需要安装这个工具。
使用以下命令安装:
$ sudo yum install virtualenv
下一步是使用 virtualenv 命令在 flasky 文件夹中创建 Python 虚拟环境。这个命令只有一 个必需的参数,即虚拟环境的名字。创建虚拟环境后,当前文件夹中会出现一个子文件 夹,名字就是上述命令中指定的参数,与虚拟环境相关的文件都保存在这个子文件夹中。 按照惯例,一般虚拟环境会被命名为 venv:
$ virtualenv venv
New python executable in venv/bin/python2.7
Also creating executable in venv/bin/python
Installing setuptools............done.
Installing pip...............done.
现在,flasky文件夹中就有了一个名为venv的子文件夹,它保存一个全新的虚拟环境,其 中有一个私有的 Python 解释器。在使用这个虚拟环境之前,你需要先将其“激活”。如果 你使用 bash 命令行(Linux 和 Mac OS X 用户),可以通过下面的命令激活这个虚拟环境:
$ source venv/bin/activate
虚拟环境被激活后,其中 Python 解释器的路径就被添加进 PATH 中,但这种改变不是永久 性的,它只会影响当前的命令行会话。为了提醒你已经激活了虚拟环境,激活虚拟环境的 命令会修改命令行提示符,加入环境名:
(venv) $
当虚拟环境中的工作完成后,如果你想回到全局 Python 解释器中,可以在命令行提示符下 输入
deactivate
##1.2 使用pip安装Python包
大多数 Python 包都使用 pip 实用工具安装,使用 virtualenv 创建虚拟环境时会自动安装 pip。激活虚拟环境后,pip 所在的路径会被添加进 PATH。
执行下述命令可在虚拟环境中安装 Flask:
(venv) $ pip install flask
执行上述命令,你就在虚拟环境中安装 Flask 及其依赖了。要想验证 Flask 是否正确安装, 你可以启动 Python 解释器,尝试导入 Flask:
(venv) $ python
>>> import flask
>>>
如果没有看到错误提醒,就说明Flask安装完毕。
#2 程序的基本结构
##2.1 初始化
所有 Flask 程序都必须创建一个程序实例。Web 服务器使用一种名为 Web 服务器网关接口 (Web Server Gateway Interface,WSGI)的协议,把接收自客户端的所有请求都转交给这 个对象处理。程序实例是 Flask 类的对象,经常使用下述代码创建:
from flask import Flask
app = Flask(__name__)
Flask 类的构造函数只有一个必须指定的参数,即程序主模块或包的名字。在大多数程序 中,Python 的 ‘name‘ 变量就是所需的值。
##2.2 路由和视图函数
客户端(例如 Web 浏览器)把请求发送给 Web 服务器,Web 服务器再把请求发送给 Flask程序实例。程序实例需要知道对每个 URL 请求运行哪些代码,所以保存了一个 URL 到 Python 函数的映射关系。处理URL和函数之间关系的程序称为路由。
路由可以理解为请求的URL
在 Flask 程序中定义路由的最简便方式,是使用程序实例提供的 app.route 修饰器,把修 饰的函数注册为路由。下面的例子说明了如何使用这个修饰器声明路由:
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
上例把 index() 函数注册为程序根地址的处理程序。如果部署程序的服务器域名为 www. example.com,在浏览器中访问 http://www.example.com 后,会触发服务器执行 index() 函 数。这个函数的返回值称为响应,是客户端接收到的内容。如果客户端是 Web 浏览器,响 应就是显示给用户查看的文档。
像index()这样的函数称为视图函数(view function)。视图函数返回的响应可以是包含 HTML 的简单字符串,也可以是复杂的表单。
如果你仔细观察日常所用服务的某些 URL 格式,会发现很多地址中都包含可变部分。例 如, 你 的 Facebook 资 料 页 面 的 地 址 是 http://www.facebook.com/
@app.route('/user/<name>')
def user(name):
return '<h1>Hello, %s!</h1>' % name
尖括号中的内容就是动态部分,任何能匹配静态部分的 URL 都会映射到这个路由上。调 用视图函数时,Flask 会将动态部分作为参数传入函数。在这个视图函数中,参数用于生 成针对个人的欢迎消息。
路由中的动态部分默认使用字符串,不过也可使用类型定义。例如,路由 /user/
##2.3 启动服务器
程序实例用 run 方法启动 Flask 集成的开发 Web 服务器:
if __name__ == '__main__':
app.run(debug=True)
debug模式打开的情况下如果程序出错可以在浏览器中看到报错信息
__name__=='__main__'
是 Python 的惯常用法,在这里确保直接执行这个脚本时才启动开发 Web 服务器。如果这个脚本由其他脚本引入,程序假定父级脚本会启动不同的服务器,因 此不会执行 app.run()。
服务器启动后,会进入轮询,等待并处理请求。轮询会一直运行,直到程序停止,比如按 Ctrl-C 键。
有一些选项参数可被 app.run() 函数接受用于设置 Web 服务器的操作模式。在开发过程中 启用调试模式会带来一些便利,比如说激活调试器和重载程序。要想启用调试模式,我们 可以把 debug 参数设为 True。
##2.4 一个完整的程序
前几节介绍了 Flask Web 程序的不同组成部分,现在是时候开发一个程序了。整个 hello.py 程序脚本就是把前面介绍的三部分合并到一个文件中。程序代码如示例 2-1 所示。
一个完整的 Flask 程序:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
if __name__ == '__main__':
app.run(debug=True)
打开 Web 浏览器,在地址栏中输入 http://127.0.0.1:5000/ 即可看到返回的 Hello World!
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return '<h1>Hello World!</h1>'
@app.route('/user/<name>') #动态路由
def user(name):
return '<h1>Hello, %s!</h1>' % name
if __name__ == '__main__':
app.run(debug=True)
访问 http://localhost:5000/user/Dave。 程序会显示一个使用 name 动态参数生成的欢迎消息。尝试使用不同的名字,可以看到视 图函数总是使用指定的名字生成响应.
##2.5 请求-响应循环
###2.5.1 程序和请求上下文
Flask从客户端收到请求时,要让视图函数能访问一些对象,这样才能处理请求。请求对 象就是一个很好的例子,它封装了客户端发送的 HTTP 请求。
要想让视图函数能够访问请求对象,一个显而易见的方式是将其作为参数传入视图函数, 不过这会导致程序中的每个视图函数都增加一个参数。除了访问请求对象,如果视图函数在处理请求时还要访问其他对象,情况会变得更糟。
为了避免大量可有可无的参数把视图函数弄得一团糟,Flask 使用上下文临时把某些对象 变为全局可访问。有了上下文,就可以写出下面的视图函数:
from flask import request
@app.route('/')
def index():
user_agent = request.headers.get('User-Agent')
return '<p>Your browser is %s</p>' % user_agent
request.headers.get(‘User-Agent’)
可以获得客户端发过来的请求的请求头中的User-Agent
变量名 | 上下文 | 说明 |
---|---|---|
current_app | 程序上下文 | 当前激活程序的程序实例 |
g | 程序上下文 | 处理请求时用作临时存储的对象。每次请求都会重设这个变量 |
request | 请求上下文 | 请求对象,封装了客户端发出的 HTTP 请求中的内容 |
session | 请求上下文 | 用户会话,用于存储请求之间需要“记住”的值的词典 |
注意在这个视图函数中我们如何把 request 当作全局变量使用。事实上,request不可能是全局变量。试想,在多线程服务器中,多个线程同时处理不同客户端发送的不同请求时, 每个线程看到的 request 对象必然不同。Falsk 使用上下文让特定的变量在一个线程中全局 可访问,与此同时却不会干扰其他线程
在 Flask 中有两种上下文:程序上下文和请求上下文。
Flask 在分发请求之前激活(或推送)程序和请求上下文,请求处理完成后再将其删除。程 序上下文被推送后,就可以在线程中使用 current_app 和 g 变量。类似地,请求上下文被 推送后,就可以使用 request 和 session 变量。如果使用这些变量时我们没有激活程序上 下文或请求上下文,就会导致错误。
###2.5.2 请求调度
程序收到客户端发来的请求时,要找到处理该请求的视图函数。为了完成这个任务,Flask 会在程序的 URL 映射中查找请求的 URL。URL 映射是 URL 和视图函数之间的对应关系。 Flask 使用 app.route 修饰器或者非修饰器形式的 app.add_url_rule() 生成映射。
要想查看 Flask 程序中的 URL 映射是什么样子,我们可以在 Python shell 中检查为 hello.py 生成的映射。测试之前,请确保你激活了虚拟环境:
(venv) $ python
>>> from hello import app
>>> app.url_map Map([<Rule '/' (HEAD, OPTIONS, GET) -> index>, <Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>, <Rule '/user/<name>' (HEAD, OPTIONS, GET) -> user>])
/ 和 /user/
URL 映射中的 HEAD、Options、GET 是请求方法,由路由进行处理。Flask 为每个路由都指 定了请求方法,这样不同的请求方法发送到相同的 URL 上时,会使用不同的视图函数进 行处理。HEAD 和 OPTIONS 方法由 Flask 自动处理,因此可以这么说,在这个程序中,URL 映射中的 3 个路由都使用 GET 方法。
2.5.3 请求钩子
有时在处理请求之前或之后执行代码会很有用。例如,在请求开始时,我们可能需要创 建数据库连接或者认证发起请求的用户。为了避免在每个视图函数中都使用重复的代码, Flask 提供了注册通用函数的功能,注册的函数可在请求被分发到视图函数之前或之后 调用。
请求钩子使用修饰器实现。Flask 支持以下 4 种钩子。
• :注册一个函数,在处理第一个请求之前运行。 before_request
• :注册一个函数,在每次请求之前运行。 after_request
• :注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行。 teardown_request
• :注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行。
在请求钩子函数和视图函数之间共享数据一般使用上下文全局变量 g。例如,before_ request 处理程序可以从数据库中加载已登录用户,并将其保存到 g.user 中。随后调用视 图函数时,视图函数再使用 g.user 获取用户。
###2.5.4 响应
Flask 调用视图函数后,会将其返回值作为响应的内容。大多数情况下,响应就是一个简 单的字符串,作为 HTML 页面回送客户端。
但 HTTP 协议需要的不仅是作为请求响应的字符串。HTTP 响应中一个很重要的部分是状 态码,Flask 默认设为 200,这个代码表明请求已经被成功处理。
如果视图函数返回的响应需要使用不同的状态码,那么可以把数字代码作为第二个返回 值,添加到响应文本之后。例如,下述视图函数返回一个 400 状态码,表示请求无效:
@app.route('/')
def index():
return '<h1>Bad Request</h1>', 400
视图函数返回的响应还可接受第三个参数,这是一个由首部(header)组成的字典,可以 添加到 HTTP 响应中。
如果不想返回由 1 个、2 个或 3 个值组成的元组,Flask 视图函数还可以返回 Response对象。make_response() 函数可接受 1 个、2 个或 3 个参数(和视图函数的返回值一样),并返回一个 Response对象。有时我们需要在视图函数中进行这种转换,然后在响应对象上调用各种方法,进一步设置响应。下例创建了一个响应对象,然后设置了 cookie:
from flask import make_response
@app.route('/')
def index():
response = make_response('<h1>This document carries a cookie!</h1>')
response.set_cookie('answer', '42')
return response
有一种名为重定向的特殊响应类型。这种响应没有页面文档,只告诉浏览器一个新地址用 以加载新页面。
重定向经常使用 302 状态码表示,指向的地址由 Location 首部提供。重定向响应可以使用 3 个值形式的返回值生成,也可在 Response 对象中设定。不过,由于使用频繁,Flask 提 供了 redirect() 辅助函数,用于生成这种响应:
from flask import redirect
@app.route('/')
def index():
return redirect('http://www.example.com')
还有一种特殊的响应由 abort 函数生成,用于处理错误。在下面这个例子中,如果 URL 中 动态参数 id 对应的用户不存在,就返回状态码 404:
from flask import abort
@app.route('/user/<id>')
def get_user(id):
user = load_user(id)
if not user:
abort(404)
return '<h1>Hello, %s</h1>' % user.name
注意,abort 不会把控制权交还给调用它的函数,而是抛出异常把控制权交给 Web 服 务器。
#3. 模板
##3.1 Jinja2模板引擎
形式最简单的 Jinja2 模板就是一个包含响应文本的文件。
templates/index.html:Jinja2 模板
<h1>Hello World!</h1>
templates/user.html:Jinja2 模板(带有变量)
<h1>Hello, {{ name }}!</h1>
##3.1.1 渲染模板
默认情况下,Flask 在程序文件夹中的 templates 子文件夹中寻找模板。在下一个 hello.py 版本中,要把前面定义的模板保存在 templates 文件夹中,并分别命名为 index.html 和 user. html。
from flask import Flask, render_template
# ...
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/<name>')
def user(name):
return render_template('user.html', name=name)
Flask 提供的 render_template 函数把 Jinja2 模板引擎集成到了程序中。render_template 函 数的第一个参数是模板的文件名。随后的参数都是键值对,表示模板中变量对应的真实值。在这段代码中,第二个模板收到一个名为 name 的变量。
前例中的 name=name 是关键字参数,这类关键字参数很常见,但如果你不熟悉它们的话, 可能会觉得迷惑且难以理解。左边的“name”表示参数名,就是模板中使用的占位符;右 边的“name”是当前作用域中的变量,表示同名参数的值。
##3.1.2 变量
在模板中使用的结构表示一个变量,它是一种特殊的占位符,告诉模 板引擎这个位置的值从渲染模板时使用的数据中获取。
Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。在模板 中使用变量的一些示例如下:
<p>A value from a dictionary: {{ mydict['key'] }}.</p>
<p>A value from a list: {{ mylist[3] }}.</p>
<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>
可以使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述 模板以首字母大写形式显示变量 name 的值:
Hello, {{ name|capitalize }}
过滤器名 | 说 明 |
---|---|
safe | 渲染值时不转义 |
capitalize | 把值的首字母转换成大写,其他字母转换成小写 |
lower | 把值转换成小写形式 |
upper | 把值转换成大写形式 |
title | 把值中每个单词的首字母都转换成大写 |
trim | 把值的首尾空格去掉 |
striptags | 渲染之前把值中所有的 HTML 标签都删掉 |
safe 过滤器值得特别说明一下。默认情况下,出于安全考虑,Jinja2 会转义所有变量。
例 如,如果一个变量的值为 '<h1>Hello</h1>',
Jinja2 会将其渲染成 '<h1>Hello</ h1>',
浏览器能显示这个 h1 元素,但不会进行解释。
很多情况下需要显示变量中存储 的 HTML 代码,这时就可使用 safe 过滤器。
千万别在不可信的值上使用 safe 过滤器,例如用户在表单中输入的文本。
###3.1.3 控制结构
Jinja2 提供了多种控制结构,可用来改变模板的渲染流程。本节使用简单的例子介绍其中 最有用的控制结构。
下面这个例子展示了如何在模板中使用条件控制语句:
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
另一种常见需求是在模板中渲染一组元素。下例展示了如何使用 for 循环实现这一需求:
<ul>
{% for comment in comments %}
{{ comment }}
{% endfor %}
</ul>
Jinja2 还支持宏。宏类似于 Python 代码中的函数。例如:
{% macro render_comment(comment) %}
{{ comment }}
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
为了重复使用宏,我们可以将其保存在单独的文件中,然后在需要使用的模板中导入:
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
`
需要在多处重复使用的模板代码片段可以写入单独的文件,再包含在所有模板中,以避免 重复:
{% include 'common.html' %}
另一种重复使用代码的强大方式是模板继承,它类似于 Python 代码中的类继承。首先,创 建一个名为 base.html 的基模板:
<html>
<head>
{% block head %}
{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
block 标签定义的元素可在衍生模板中修改。在本例中,我们定义了名为 head、title 和 body 的块。注意,title 包含在 head 中。下面这个示例是基模板的衍生模板:
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }}
{% endblock %}
{% block body %}
Hello, World!
{% endblock %}
extends 指令声明这个模板衍生自 base.html。在 extends 指令之后,基模板中的 3 个块被 重新定义,模板引擎会将其插入适当的位置。注意新定义的 head 块,在基模板中其内容不 是空的,所以使用 super() 获取原来的内容。
##3.3 自定义错误页面
如果你在浏览器的地址栏中输入了不可用的路由,那么会显示一个状态码为 404 的错误页 面。现在这个错误页面太简陋、平庸,而且样式和使用了 Bootstrap 的页面不一致。
像常规路由一样,Flask 允许程序使用基于模板的自定义错误页面。最常见的错误代码有 两个:404,客户端请求未知页面或路由时显示;500,有未处理的异常时显示。为这两个 错误代码指定自定义处理程序的方式如示例 3-6 所示。
自定义错误页面
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
和视图函数一样,错误处理程序也会返回响应。它们还返回与该错误对应的数字状态码。
错误处理程序中引用的模板也需要编写。这些模板应该和常规页面使用相同的布局,因此 要有一个导航条和显示错误消息的页面头部。
编写这些模板最直观的方法是复制 templates/user.html,分别创建 templates/404.html 和
templates/500.html,然后把这两个文件中的页面头部元素改为相应的错误消息。但这种方 法会带来很多重复劳动。
Jinja2 的模板继承机制可以帮助我们解决这一问题。Flask-Bootstrap 提供了一个具有页面基 本布局的基模板,同样,程序可以定义一个具有更完整页面布局的基模板,其中包含导航 条,而页面内容则可留到衍生模板中定义。示例 3-7 展示了 templates/base.html 的内容,这 是一个继承自 bootstrap/base.html 的新模板,其中定义了导航条。这个模板本身也可作为其 他模板的基模板,例如 templates/user.html、templates/404.html 和 templates/500.html。
示例 3-7 templates/base.html:包含导航条的程序基模板
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block navbar %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
{% endblock %}
{% block content %}
这个模板的content块中只有一个<div>
容器,其中包含了一个名为page_content的新的空块,块中的内容由衍生模板定义。
现在,程序使用的模板继承自这个模板,而不直接继承自Flask-Bootstrap的基模板。通过继承templates/base.html 模板编写自定义的 404 错误页面很简单
示例 3-8 templates/404.html:使用模板继承机制自定义 404 错误页面
{% extends "base.html" %}
{% block title %}Flasky - Page Not Found{% endblock %}
{% block page_content %}
Not Found
{% endblock %}
错误页面在浏览器中的显示效果如图 3-2 所示。
templates/user.html 现在可以通过继承这个基模板来简化内容,如示例 3-9 所示。
示例 3-9 templates/user.html:使用模板继承机制简化页面模板
{% extends "base.html" %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
Hello, {{ name }}!
{% endblock %}
##3.4 链接
任何具有多个路由的程序都需要可以连接不同页面的链接,例如导航条。
在模板中直接编写简单路由的 URL 链接不难,但对于包含可变部分的动态路由,在模板 中构建正确的 URL 就很困难。而且,直接编写 URL 会对代码中定义的路由产生不必要的 依赖关系。如果重新定义路由,模板中的链接可能会失效。
为了避免这些问题,Flask 提供了 url_for() 辅助函数,它可以使用程序 URL 映射中保存 的信息生成 URL。
url_for() 函数最简单的用法是以视图函数名(或者 app.add_url_route() 定义路由时使用 的端点名)作为参数,返回对应的 URL。例如,在当前版本的 hello.py 程序中调用 url_for(‘index’) 得到的结果是/。调用 url_for(‘index’, _external=True) 返回的则是绝对地址,在这个示例中是 http://localhost:5000/。
##3.5 静态文件
Web 程序不是仅由 Python 代码和模板组成。大多数程序还会使用静态文件,例如 HTML 代码中引用的图片、JavaScript 源码文件和 CSS。
默认设置下,Flask 在程序根目录中名为 static 的子目录中寻找静态文件。如果需要,可在 static 文件夹中使用子文件夹存放文件。服务器收到前面那个 URL 后,会生成一个响应, 包含文件系统中 static/css/styles.css 文件的内容。
示例 3-10 展示了如何在程序的基模板中放置 favicon.ico 图标。这个图标会显示在浏览器的 地址栏中。
示例 3-10 templates/base.html:定义收藏夹图标
{% block head %}
{{ super() }}
{% endblock %}
图标的声明会插入 head 块的末尾。注意如何使用 super() 保留基模板中定义的块的原始 内容。
Web表单
请求对象包含客户端发出的所有请求信息。其中,request.form 能获取 POST 请求中提交的表单数据。
尽管 Flask 的请求对象提供的信息足够用于处理 Web 表单,但有些任务很单调,而且要重 复操作。比如,生成表单的 HTML 代码和验证提交的表单数据。
Flask-WTF(http://pythonhosted.org/Flask-WTF/)扩展可以把处理 Web 表单的过程变成一 种愉悦的体验。这个扩展对独立的 WTForms(http://wtforms.simplecodes.com)包进行了包 装,方便集成到 Flask 程序中。
Flask-WTF 及其依赖可使用 pip 安装:
(venv) $ pip install flask-wtf
##4.1 跨站请求伪造保护
默认情况下,Flask-WTF 能保护所有表单免受跨站请求伪造(Cross-Site Request Forgery, CSRF)的攻击。恶意网站把请求发送到被攻击者已登录的其他网站时就会引发 CSRF 攻击。
为了实现 CSRF 保护,Flask-WTF 需要程序设置一个密钥。Flask-WTF 使用这个密钥生成 加密令牌,再用令牌验证请求中表单数据的真伪。设置密钥的方法如示例 4-1 所示。
示例 4-1 hello.py:设置 Flask-WTF
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'
app.config 字典可用来存储框架、扩展和程序本身的配置变量。使用标准的字典句法就能 把配置值添加到 app.config 对象中。这个对象还提供了一些方法,可以从文件或环境中导 入配置值。
SECRET_KEY 配置变量是通用密钥,可在 Flask 和多个第三方扩展中使用。如其名所示,加 密的强度取决于变量值的机密程度。不同的程序要使用不同的密钥,而且要保证其他人不 知道你所用的字符串。
##4.2 表单类
使用 Flask-WTF 时,每个 Web 表单都由一个继承自 Form 的类表示。这个类定义表单中的 一组字段,每个字段都用对象表示。字段对象可附属一个或多个验证函数。验证函数用来 验证用户提交的输入值是否符合要求。
示例 4-2 是一个简单的 Web 表单,包含一个文本字段和一个提交按钮。
示例 4-2 hello.py:定义表单类
from flask.ext.wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required
class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')
这个表单中的字段都定义为类变量,类变量的值是相应字段类型的对象。在这个示例中, NameForm 表单中有一个名为 name 的文本字段和一个名为 submit 的提交按钮。StringField 类表示属性为 type=”text” 的 <input>
元素。SubmitField 类表示属性为 type=”submit” 的 <input>
元素。字段构造函数的第一个参数是把表单渲染成 HTML 时使用的标号。
StringField 构造函数中的可选参数 validators 指定一个由验证函数组成的列表,在接受 用户提交的数据之前验证数据。验证函数 Required() 确保提交的字段不为空。
WTForms 支持的 HTML 标准字段
字段类型 | 说 明 |
---|---|
StringField | 文本字段 |
TextAreaField | 多行文本字段 |
PasswordField | 密码文本字段 |
HiddenField | 隐藏文本字段 |
DateField | 文本字段,值为 datetime.date 格式 |
DateTimeField | 文本字段,值为 datetime.datetime 格式 |
IntegerField | 文本字段,值为整数 |
DecimalField | 文本字段,值为 decimal.Decimal |
FloatField | 文本字段,值为浮点数 |
BooleanField | 复选框,值为 True 和 False |
RadioField | 一组单选框 |
SelectField | 下拉列表 |
SelectMultipleField | 下拉列表,可选择多个值 |
FileField | 文件上传字段 |
SubmitField | 表单提交按钮 |
FormField | 把表单作为字段嵌入另一个表单 |
FieldList | 一组指定类型的字段 |
WTForms 内建的验证函数
验证函数 | 说 明 |
---|---|
验证电子邮件地址 | |
EqualTo | 比较两个字段的值;常用于要求输入两次密码进行确认的情况 |
IPAddress | 验证 IPv4 网络地址 |
Length | 验证输入字符串的长度 |
NumberRange | 验证输入的值在数字范围内 |
Optional | 无输入值时跳过其他验证函数 |
Required | 确保字段中有数据 |
Regexp | 使用正则表达式验证输入值 |
URL | 验证 URL |
AnyOf | 确保输入值在可选值列表中 |
NoneOf | 确保输入值不在可选值列表中 |
##4.3 把表单渲染成HTML
表单字段是可调用的,在模板中调用后会渲染成 HTML。假设视图函数把一个 NameForm 实例通过参数 form 传入模板,在模板中可以生成一个简单的表单,如下所示:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }}
{{ form.name() }}
{{ form.submit() }}
</form>
当然,这个表单还很简陋。要想改进表单的外观,可以把参数传入渲染字段的函数,传入 的参数会被转换成字段的 HTML 属性。例如,可以为字段指定 id 或 class 属性,然后定义CSS 样式:
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }}
{{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>
即便能指定 HTML 属性,但按照这种方式渲染表单的工作量还是很大,所以在条件允许的 情况下最好能使用 Bootstrap 中的表单样式。Flask-Bootstrap 提供了一个非常高端的辅助函 数,可以使用 Bootstrap 中预先定义好的表单样式渲染整个 Flask-WTF 表单,而这些操作 只需一次调用即可完成。使用 Flask-Bootstrap,上述表单可使用下面的方式渲染:
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
import 指令的使用方法和普通 Python 代码一样,允许导入模板中的元素并用在多个模板 中。导入的 bootstrap/wtf.html 文件中定义了一个使用 Bootstrap 渲染 Falsk-WTF 表单对象 的辅助函数。wtf.quick_form() 函数的参数为 Flask-WTF 表单对象,使用 Bootstrap 的默认 样式渲染传入的表单。hello.py 的完整模板如示例 4-3 所示。
示例 4-3 templates/index.html:使用 Flask-WTF 和 Flask-Bootstrap 渲染表单
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
Hello, {% if name %}{{ name }} {% else %}Stranger{% endif %}!
{{ wtf.quick_form(form) }}
{% endblock %}
模板的内容区现在有两部分。第一部分是页面头部,显示欢迎消息。这里用到了一个模板 条件语句。Jinja2 中的条件语句格式为 ...。 如果条件的计算结果为 True,那么渲染 if 和 else 指令之间的值。如果条件的计算结果为 False,则渲染 else 和 endif 指令之间的值。在这个例子中,如果没有定义模板变量 name,
则会渲染字符串“Hello, Stranger!”。内容区的第二部分使用 wtf.quick_form() 函数渲染 NameForm 对象。
##4.4 在视图函数中处理表单
这一次视图函数 index() 不仅要渲染表单,还要接收表单中的数据。
示例 4-4 是更新后的 index() 视图函数。
示例 4-4 hello.py:路由方法
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)
app.route 修饰器中添加的 methods 参数告诉 Flask 在 URL 映射中把这个视图函数注册为 GET 和 POST 请求的处理程序。如果没指定 methods 参数,就只把视图函数注册为 GET 请求 的处理程序。
把 POST 加入方法列表很有必要,因为将提交表单作为 POST 请求进行处理更加便利。表单 也可作为 GET 请求提交,不过 GET 请求没有主体,提交的数据以查询字符串的形式附加到 URL 中,可在浏览器的地址栏中看到。基于这个以及其他多个原因,提交表单大都作为 POST 请求进行处理。
局部变量 name 用来存放表单中输入的有效名字,如果没有输入,其值为 None。如上述代 码所示,在视图函数中创建一个 NameForm 类实例用于表示表单。提交表单后,如果数据能 被所有验证函数接受,那么 validate_on_submit() 方法的返回值为 True,否则返回 False。 这个函数的返回值决定是重新渲染表单还是处理表单提交的数据。
用户第一次访问程序时,服务器会收到一个没有表单数据的 GET 请求,所以 validateon submit() 将返回 False。if 语句的内容将被跳过,通过渲染模板处理请求,并传入表单对 象和值为 None 的 name 变量作为参数。用户会看到浏览器中显示了一个表单。
用户提交表单后,服务器收到一个包含数据的 POST 请求。validate_on_submit() 会调用 name 字段上附属的 Required() 验证函数。如果名字不为空,就能通过验证,validateon submit() 返回 True。现在,用户输入的名字可通过字段的 data 属性获取。在 if 语句中, 把名字赋值给局部变量 name,然后再把 data 属性设为空字符串,从而清空表单字段。最 后一行调用 render_template() 函数渲染模板,但这一次参数 name 的值为表单中输入的名 字,因此会显示一个针对该用户的欢迎消息。